defmodule Cypher do
  @moduledoc false
  alias Bolt.Sips, as: Neo4j
  require Logger

  def query(q) do
    Neo4j.query(Neo4j.conn(), q)
  end

  def query(q, params) do
    Neo4j.query(Neo4j.conn(), q, params)
  end

  @doc """
  for set, create instructions as final state of the query
  """
  def write_tf(q) do
    with {:ok, response} <- Neo4j.query(Neo4j.conn(), q),
         {:ok, s} <- Map.fetch(response, :stats) do
      if Enum.empty?(s), do: :failed, else: :succeed
    else
      {:error, reason} ->
        Logger.error("write_tf Error, reason: #{inspect(reason)}")
        :failed
      _ ->
        :failed
    end
  end

  def write_tf(q, params) do
    with {:ok, response} <- Neo4j.query(Neo4j.conn(), q, params),
         {:ok, s} <- Map.fetch(response, :stats) do
      if Enum.empty?(s), do: :failed, else: :succeed
    else
      {:error, reason} ->
        Logger.error("write_tf Error, reason: #{inspect(reason)}")
        :failed
      _ ->
        :failed
    end
  end

  @doc """
  for state such as exists(), count(*)>0 as the end of the query.
  exp: return count(*)>0 as allow
  """
  def tf(q) do
    with {:ok, %{:results => [response]}} <- Neo4j.query(Neo4j.conn(), q) do
      [p] = Map.values(response)
      p
    else
      {:error, reason} ->
        Logger.error("tf Error, reason: #{inspect(reason)}")
        false
      _ ->
        false
    end
  end

  def tf(q, params) do
    with {:ok, %{:results => [response]}} <- Neo4j.query(Neo4j.conn(), q, params) do
      [p] = Map.values(response)
      p
    else
      {:error, reason} ->
        Logger.error("tf Error, reason: #{inspect(reason)}")
        false
      _ ->
        false
    end
  end

  def whole_results(q) do
    with {:ok, %{:results => results}} <- Neo4j.query(Neo4j.conn(), q) do
      results
    else
      {:error, reason} ->
        Logger.error("whole_results Error, reason: #{inspect(reason)}")
        []
    end
  end

  def whole_results(q, params) do
    with {:ok, %{:results => results}} <- Neo4j.query(Neo4j.conn(), q, params) do
      results
    else
      {:error, reason} ->
        Logger.error("whole_results Error, reason: #{inspect(reason)}")
        []
    end
  end

  @doc """
  "match ... return p;"
  %{:results => %{"p"=>value}
  got: value
  """
  def query_single_results_value(q) do
    with {:ok, %{:results => nrs}} <- Neo4j.query(Neo4j.conn(), q) do
      Enum.reduce(nrs, [], fn m, acc ->
        [p] = Map.values(m)
        [p | acc]
      end)
    else
      {:error, reason} ->
        Logger.error("query_single_results_value Error, reason: #{inspect(reason)}")
        []
    end
  end

  def query_single_results_value(q, params) do
    with {:ok, %{:results => nrs}} <- Neo4j.query(Neo4j.conn(), q, params) do
      Enum.reduce(nrs, [], fn m, acc ->
        [p] = Map.values(m)
        [p | acc]
      end)
    else
      {:error, reason} ->
        Logger.error("query_single_results_value Error, reason: #{inspect(reason)}")
        []
    end
  end

  def property(q) do
    with {:ok, %{:results => [nr]}} <- Neo4j.query(Neo4j.conn(), q) do
      [p] = Map.values(nr)
      p.properties
    else
      {:error, reason} ->
        Logger.error("property Error, reason: #{inspect(reason)}")
        nil
      _ ->
        nil
    end
  end

  def property(q, params) do
    with {:ok, %{:results => [nr]}} <- Neo4j.query(Neo4j.conn(), q, params) do
      [p] = Map.values(nr)
      p.properties
    else
      {:error, reason} ->
        Logger.error("property Error, reason: #{inspect(reason)}")
        nil
      _ ->
        nil
    end
  end

  def properties(q) do
    with {:ok, %{:results => nrs}} <- Neo4j.query(Neo4j.conn(), q) do
      Enum.reduce(nrs, [], fn result, acc ->
        [p] =
          Map.values(result)
          |> Enum.into([], fn nr -> nr.properties end)

        [p | acc]
      end)
    else
      {:error, reason} ->
        Logger.error("properties Error, reason: #{inspect(reason)}")
        []
    end
  end

  def properties(q, params) do
    with {:ok, %{:results => nrs}} <- Neo4j.query(Neo4j.conn(), q, params) do
      Enum.reduce(nrs, [], fn result, acc ->
        [p] =
          Map.values(result)
          |> Enum.into([], fn nr -> nr.properties end)

        [p | acc]
      end)
    else
      {:error, reason} ->
        Logger.error("properties Error, reason: #{inspect(reason)}")
        []
    end
  end

  def build_update_cypher(var_name, info, except \\ []) do
    str =
      Enum.reduce(info, "SET ", fn {k, v}, str ->
        v_str = if is_list(v), do: "#{inspect(v, charlists: :as_list)}", else: "#{inspect(v)}"

        if k in except do
          str
        else
          if str == "SET " do
            str <> var_name <> "." <> k <> " = #{v_str}"
          else
            str <> ", " <> var_name <> "." <> k <> " = #{v_str}"
          end
        end
      end)

    if str == "SET " do
      ""
    else
      str
    end
  end

  def build_label_str(labels, type) do
    if type == :node do
      Enum.reduce(labels, "", fn l, acc ->
        acc <> ":#{l}"
      end)
    else
      [l | _] = labels
      ":#{l}"
    end
  end

  def build_property_str(properties) do
    s =
      Enum.reduce(properties, "", fn {k, v}, acc ->
        content =
          if is_list(v) do
            "#{k}:#{inspect(v, charlists: :as_list)}"
          else
            "#{k}:#{inspect(v)}"
          end

        if acc == "" do
          content
        else
          acc <> ", " <> content
        end
      end)

    if s != "", do: "{#{s}}", else: ""
  end

  def create_node(labels, properties) when is_list(labels) and is_map(properties) do
    label_str = build_label_str(labels, :node)
    set_str = build_update_cypher("n", properties)
    "create (n#{label_str}) #{set_str}" |> write_tf
  end

  def delete_node(labels_or_id, filter \\ %{}) when is_map(filter) do
    match = build_match("n", labels_or_id, :node, filter)

    if match != nil do
      q = match <> " detach delete n"
      Logger.debug("q: #{inspect(q)}")
      q |> write_tf
    end
  end

  def create_relation({n1_label, n1_filter}, {n2_label, n2_filter}, {r_label, r_property}) do
    connect_nodes({n1_label, n1_filter}, {n2_label, n2_filter}, {r_label, r_property})
  end

  def delete_relation({n1_label, n1_filter}, {n2_label, n2_filter}, {r_label, r_property}) do
    n1_l_str = build_label_str(n1_label, :node)
    n2_l_str = build_label_str(n2_label, :node)
    r_l_str = build_label_str(r_label, :relation)
    n1_f_str = build_property_str(n1_filter)
    n2_f_str = build_property_str(n2_filter)
    r_f_str = build_property_str(r_property)

    """
    match (n1#{n1_l_str} #{n1_f_str})-[r#{r_l_str} #{r_f_str}]-(n1#{n2_l_str} #{n2_f_str})
    delete r;
    """
    |> write_tf
  end

  def delete_relation({n1_label, n1_filter}, {n2_label, n2_filter}) do
    disconnect_nodes({n1_label, n1_filter}, {n2_label, n2_filter})
  end

  def delete_relation(labels_or_id, filter) do
    match = build_match("n", labels_or_id, :relation, filter)

    if match != nil do
      q = match <> " detach delete n"
      Logger.debug("q: #{inspect(q)}")
      q |> write_tf
    else
      :failed
    end
  end

  def connect_nodes({n1_label, n1_filter}, {n2_label, n2_filter}, {r_label, r_property}) do
    n1_l_str = build_label_str(n1_label, :node)
    n2_l_str = build_label_str(n2_label, :node)
    r_l_str = build_label_str(r_label, :relation)
    n1_f_str = build_property_str(n1_filter)
    n2_f_str = build_property_str(n2_filter)
    r_f_str = build_property_str(r_property)

    """
    match (n1#{n1_l_str} #{n1_f_str})
    match (n2#{n2_l_str} #{n2_f_str})
    merge (n1)-[#{r_l_str} #{r_f_str}]->(n2)
    """
    |> write_tf
  end

  def disconnect_nodes({n1_label, n1_filter}, {n2_label, n2_filter}) do
    n1_l_str = build_label_str(n1_label, :node)
    n2_l_str = build_label_str(n2_label, :node)
    n1_f_str = build_property_str(n1_filter)
    n2_f_str = build_property_str(n2_filter)

    q = """
    match (n1#{n1_l_str} #{n1_f_str})-[r]-(n2#{n2_l_str} #{n2_f_str})
    delete r
    """

    Logger.debug("q: #{inspect(q)}")
    q |> write_tf
  end

  def relations_between({n1_label, n1_filter}, {n2_label, n2_filter}) do
    n1_l_str = build_label_str(n1_label, :node)
    n2_l_str = build_label_str(n2_label, :node)
    n1_f_str = build_property_str(n1_filter)
    n2_f_str = build_property_str(n2_filter)

    q = """
    match (n1#{n1_l_str} #{n1_f_str})-[r]-(n2#{n2_l_str} #{n2_f_str})
    return r
    """

    case q |> query() do
      {:ok, %{:results => results}} ->
        Enum.reduce(results, [], fn m, acc ->
          [Map.values(m) | acc]
        end)
        |> List.flatten()

      _ ->
        []
    end
  end

  def add_property(labels_or_id, properties, filter \\ %{}, type \\ :node) do
    match = build_match("n", labels_or_id, type, filter)
    set_str = build_update_cypher("n", properties)

    if match != nil do
      q = match <> " #{set_str}"
      Logger.debug("q: #{inspect(q)}")
      q |> query
    else
      :failed
    end
  end

  def update_property(labels_or_id, properties, filter \\ %{}, type \\ :node) do
    add_property(labels_or_id, properties, filter, type)
  end

  def delete_property(labels_or_id, properties, filter \\ %{}, type \\ :node)
      when is_list(properties) do
    ps =
      Enum.reduce(properties, "", fn p, acc ->
        if acc == "" do
          "n.#{p}"
        else
          acc <> ", n.#{p}"
        end
      end)

    match = build_match("n", labels_or_id, type, filter)

    if match != "" do
      q = match <> " REMOVE #{ps}"
      Logger.debug("q: #{inspect(q)}")
      q |> query
    else
      :failed
    end
  end

  def replace_property(labels_or_id, property_map, filter \\ %{}, type \\ :node) do
    {replace_str, old_ps} =
      Enum.reduce(property_map, {"", ""}, fn {k, v}, {acc_r, acc_o} ->
        if acc_r == "" do
          {"n.#{v} = n.#{k}", "n.#{k}"}
        else
          {acc_r <> ", n.#{v} = n.#{k}", acc_o <> ", n.#{k}"}
        end
      end)

    match = build_match("n", labels_or_id, type, filter)
    Logger.debug("match: #{inspect(match)}")

    if match != nil do
      q = """
      #{match}
      SET #{replace_str}
      REMOVE #{old_ps}
      """

      Logger.debug("q: #{inspect(q)}")
      q |> write_tf
    else
      :failed
    end
  end

  def node_labels(labels_or_id, filter \\ %{}) do
    match = build_match("n", labels_or_id, :node, filter)

    if match != nil do
      q = "#{match} return n"
      Logger.debug("q: #{inspect(q)}")

      case q |> query do
        {:ok, response} ->
          response |> Cypher.ResponseHelper.node_labels()

        _ ->
          :failed
      end
    else
      :failed
    end
  end

  def relation_type(type_or_id, filter \\ %{}) do
    match = build_match("r", type_or_id, :relation, filter)

    if match != nil do
      q = "#{match} return r"
      Logger.debug("q: #{inspect(q)}")

      case q |> query do
        {:ok, response} ->
          response |> Cypher.ResponseHelper.relation_type()

        _ ->
          :failed
      end
    else
      :failed
    end
  end

  def relabel_node(labels_or_id, old_labels, new_labels, filter \\ %{}) do
    match = build_match("n", labels_or_id, :node, filter)
    l_str = build_label_str(old_labels, :node)
    nl_str = build_label_str(new_labels, :node)
    "#{match} REMOVE n#{l_str} SET n#{nl_str}" |> write_tf
  end

  def retype_relation(id, new_labels) when is_integer(id) do
    nl_str = build_label_str(new_labels, :relation)

    q = """
    MATCH (n1)-[r]->(n2)
    WHERE ID(r) = #{inspect(id)}
    CREATE (n1)-[r2#{nl_str}]->(n2)
    SET r2 = r
    WITH r
    DELETE r
    """

    Logger.debug("q: #{inspect(q)}")
    q |> write_tf
  end

  def build_match(name, labels_or_id, type, filter) do
    cond do
      is_list(labels_or_id) ->
        l_str = build_label_str(labels_or_id, type)
        k_build_match(name, l_str, type, filter)

      is_integer(labels_or_id) ->
        k_build_match(name, labels_or_id, type)

      true ->
        nil
    end
  end

  defp k_build_match(name, l_str, type, filter) do
    case type do
      :node ->
        if Enum.empty?(filter) do
          "match (#{name}#{l_str})"
        else
          f_str = build_property_str(filter)
          "match (#{name}#{l_str} #{f_str})"
        end

      :relation ->
        if Enum.empty?(filter) do
          "match ()-[#{name}#{l_str}]->()"
        else
          f_str = build_property_str(filter)
          "match ()-[#{name}#{l_str} #{f_str}]->()"
        end

      _ ->
        nil
    end
  end

  defp k_build_match(name, id, type) do
    case type do
      :node ->
        "match (#{name}) where ID(#{name}) = #{inspect(id)}"

      :relation ->
        "match ()-[#{name}]->() where ID(#{name}) = #{inspect(id)}"

      _ ->
        nil
    end
  end

  def take_property(data) when is_list(data) do
    Enum.reduce(data, [], fn x, acc ->
      case Map.fetch(x, :properties) do
        {:ok, p} -> [p | acc]
        _ -> acc
      end
    end)
  end

  def take_property(data) when is_map(data) do
    Map.fetch!(data, :properties)
  end

  def take_property(_) do
    nil
  end
end
